import { computed, observable, toJS } from 'mobx'

export class SelectionModel<T> {
  @observable
  private _selection: Map<T, T> = new Map()

  // private _selected: T[] = []

  private _deselectedTo: T[] = []
  private _selectedTo: T[] = []

  @observable
  private _lastChange: SelectionChange<T> = null

  @computed.struct
  get selected(): T[] {
    // console.log('compute selected')
    return Array.from(this._selection.values())
  }

  @computed
  get isEmpty(): boolean {
    return this._selection.size === 0
  }

  @computed
  get hasValue(): boolean {
    return !this.isEmpty
  }

  @computed.struct
  get lastChange(): SelectionChange<T> {
    return toJS(this._lastChange)
  }

  constructor(private _multiple = false, initiallySelectedValues?: T[]) {
    if (initiallySelectedValues && initiallySelectedValues.length) {
      if (_multiple) {
        initiallySelectedValues.forEach(it => this._markSelected(it))
      } else {
        this._markSelected(initiallySelectedValues[ 0 ])
      }
      this._submitChange()
      this._selectedTo.length = 0
    }
  }

  select(...values: T[]): void {
    this._verifyValueAssignment(values)
    values.forEach(it => this._markSelected(it))
    this._submitChange()
  }

  deselect(...values: T[]): void {
    this._verifyValueAssignment(values)
    values.forEach(it => this._unmarkSelected(it))
    this._submitChange()
  }

  clear(): void {
    this._unmarkAll()
    this._submitChange()
  }

  toggle(value: T): void {
    this.isSelected(value) ? this.deselect(value) : this.select(value)
  }

  isSelected(value: T): boolean {
    return this._selection.has(value)
  }

  private _submitChange() {
    console.log('submit change')
    if (this._selectedTo.length || this._deselectedTo.length) {
      const next = new Map(this._selection)
      this._selectedTo.forEach(it => next.set(it, it))
      this._deselectedTo.forEach(it => next.delete(it))

      this._selection = next

      this._lastChange = new SelectionChange(this._selectedTo, this._deselectedTo)

      this._deselectedTo = []
      this._selectedTo = []
    }
  }

  private _markSelected(value: T) {
    if (!this.isSelected(value)) {
      if (!this._multiple) {
        this._unmarkAll()
      }
      this._selectedTo.push(value)
    }
  }

  private _unmarkSelected(value: T) {
    if (this.isSelected(value)) {
      this._deselectedTo.push(value)
    }
  }

  private _unmarkAll() {
    if (!this.isEmpty) {
      this._selection.forEach(it => this._unmarkSelected(it))
    }
  }

  private _verifyValueAssignment(values: T[]) {
    if (values.length > 1 && !this._multiple) {
      throw getMultipleValuesInSingleSelectionError()
    }
  }
}

export class SelectionChange<T> {
  /** Model that dispatched the event. */
  // source: SelectionModel<T>
  /** Options that were added to the model. */
  readonly added: ReadonlyArray<T>
  /** Options that were removed from the model. */
  readonly removed: ReadonlyArray<T>

  constructor(added: T[], removed: T[]) {
    this.added = Object.freeze([ ...added ])
    this.removed = Object.freeze([ ...removed ])
  }
}

// function toJS(value) {
//   return JSON.parse(JSON.stringify(value))
// }

function getMultipleValuesInSingleSelectionError() {
  return Error('Cannot pass multiple values into SelectionModel with single-value mode.')
}
